2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "ESGaimJabberAccount.h"
18 #import "SLGaimCocoaAdapter.h"
19 #import <Adium/AIAccountControllerProtocol.h>
20 #import <Adium/AIInterfaceControllerProtocol.h>
21 #import <Adium/AIStatusControllerProtocol.h>
22 #import <Adium/AIContactControllerProtocol.h>
23 #import <Adium/AIChat.h>
24 #import <Adium/AIHTMLDecoder.h>
25 #import <Adium/AIListContact.h>
26 #import <Adium/AIStatus.h>
27 #import <Adium/ESFileTransfer.h>
28 #import <Adium/ESTextAndButtonsWindowController.h>
29 #import <AIUtilities/AIAttributedStringAdditions.h>
30 #include <Libgaim/buddy.h>
31 #include <Libgaim/presence.h>
32 #include <Libgaim/si.h>
34 #define DEFAULT_JABBER_HOST @"@jabber.org"
36 extern void jabber_roster_request(JabberStream *js);
38 @implementation ESGaimJabberAccount
41 * @brief The UID will be changed. The account has a chance to perform modifications
43 * Upgrade old Jabber accounts stored with the host in a separate key to have the right UID, in the form
46 * Append @jabber.org to a proposed UID which has no domain name and does not need to be updated.
48 * @param proposedUID The proposed, pre-filtered UID (filtered means it has no characters invalid for this servce)
49 * @result The UID to use; the default implementation just returns proposedUID.
51 - (NSString *)accountWillSetUID:(NSString *)proposedUID
53 proposedUID = [proposedUID lowercaseString];
56 if ((proposedUID && ([proposedUID length] > 0)) &&
57 ([proposedUID rangeOfString:@"@"].location == NSNotFound)) {
60 //Upgrade code: grab a previously specified Jabber host
61 if ((host = [self preferenceForKey:@"Jabber:Host" group:GROUP_ACCOUNT_STATUS ignoreInheritedValues:YES])) {
62 //Determine our new, full UID
63 correctUID = [NSString stringWithFormat:@"%@@%@",proposedUID, host];
65 //Clear the preference and then set the UID so we don't perform this upgrade again
66 [self setPreference:nil forKey:@"Jabber:Host" group:GROUP_ACCOUNT_STATUS];
67 [self setPreference:correctUID forKey:@"FormattedUID" group:GROUP_ACCOUNT_STATUS];
70 //Append [self serverSuffix] (e.g. @jabber.org) to a Jabber account with no server
71 correctUID = [proposedUID stringByAppendingString:[self serverSuffix]];
74 correctUID = proposedUID;
80 - (const char*)protocolPlugin
85 - (NSSet *)supportedPropertyKeys
87 static NSMutableSet *supportedPropertyKeys = nil;
89 if (!supportedPropertyKeys) {
90 supportedPropertyKeys = [[NSMutableSet alloc] initWithObjects:
94 [supportedPropertyKeys unionSet:[super supportedPropertyKeys]];
97 return supportedPropertyKeys;
100 - (void)configureGaimAccount
102 [super configureGaimAccount];
104 NSString *connectServer;
105 BOOL forceOldSSL, allowPlaintext;
107 gaim_account_set_username(account, [self gaimAccountName]);
109 //'Connect via' server (nil by default)
110 connectServer = [self preferenceForKey:KEY_JABBER_CONNECT_SERVER group:GROUP_ACCOUNT_STATUS];
112 gaim_account_set_string(account, "connect_server", (connectServer ?
113 [connectServer UTF8String] :
116 //Force old SSL usage? (off by default)
117 forceOldSSL = [[self preferenceForKey:KEY_JABBER_FORCE_OLD_SSL group:GROUP_ACCOUNT_STATUS] boolValue];
118 gaim_account_set_bool(account, "old_ssl", forceOldSSL);
120 //Allow plaintext authorization over an unencrypted connection? Gaim will prompt if this is NO and is needed.
121 allowPlaintext = [[self preferenceForKey:KEY_JABBER_ALLOW_PLAINTEXT group:GROUP_ACCOUNT_STATUS] boolValue];
122 gaim_account_set_bool(account, "auth_plain_in_clear", allowPlaintext);
125 - (NSString *)serverSuffix
127 AILog(@"using jabber");
128 return DEFAULT_JABBER_HOST;
131 /*! @brief Obtain the resource name for this Jabber account.
133 * This could be extended in the future to perform keyword substitution (e.g. s/%computerName%/CSCopyMachineName()/).
135 * @return The resource name for the account.
137 - (NSString *)resourceName
139 return [self preferenceForKey:KEY_JABBER_RESOURCE group:GROUP_ACCOUNT_STATUS];
142 - (const char *)gaimAccountName
144 NSString *userNameWithHost = nil, *completeUserName = nil;
145 BOOL serverAppendedToUID;
148 * Gaim stores the username in the format username@server/resource. We need to pass it a username in this format
150 * The user should put the username in username@server format, which is common for Jabber. If the user does
151 * not specify the server, use jabber.org.
154 serverAppendedToUID = ([UID rangeOfString:@"@"].location != NSNotFound);
156 if (serverAppendedToUID) {
157 userNameWithHost = UID;
159 userNameWithHost = [UID stringByAppendingString:[self serverSuffix]];
162 completeUserName = [NSString stringWithFormat:@"%@/%@" ,userNameWithHost, [self resourceName]];
164 return [completeUserName UTF8String];
168 * @brief Connect Host
170 * Convenience method for retrieving the connect host for this account
172 * Rather than having a separate server field, Jabber uses the servername after the user name.
173 * username@server.org
175 * The connect server, stored in KEY_JABBER_CONNECT_SERVER, overrides this to provide the connect host. It will
176 * not be set in most cases.
182 if (!(host = [self preferenceForKey:KEY_JABBER_CONNECT_SERVER group:GROUP_ACCOUNT_STATUS])) {
183 int location = [UID rangeOfString:@"@"].location;
185 if ((location != NSNotFound) && (location + 1 < [UID length])) {
186 host = [UID substringFromIndex:(location + 1)];
189 host = [self serverSuffix];
197 * @brief Should set aliases serverside?
199 * Jabber supports serverside aliases.
201 - (BOOL)shouldSetAliasesServerside
207 * @brief Supports offline messaging?
209 * Jabber supports offline messaging.
211 - (BOOL)canSendOfflineMessageToContact:(AIListContact *)inContact
216 - (AIListContact *)contactWithUID:(NSString *)sourceUID
218 AIListContact *contact;
220 contact = [[adium contactController] existingContactWithService:service
224 contact = [[adium contactController] contactWithService:[self _serviceForUID:sourceUID]
232 - (AIService *)_serviceForUID:(NSString *)contactUID
234 AIService *contactService;
235 NSString *contactServiceID = nil;
237 if ([contactUID hasSuffix:@"@gmail.com"] ||
238 [contactUID hasSuffix:@"@googlemail.com"]) {
239 contactServiceID = @"libgaim-jabber-gtalk";
241 } else if([contactUID hasSuffix:@"@livejournal.com"]){
242 contactServiceID = @"libgaim-jabber-livejournal";
245 contactServiceID = @"libgaim-Jabber";
248 contactService = [[adium accountController] serviceWithUniqueID:contactServiceID];
250 return contactService;
255 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forListObject:(AIListObject *)inListObject
257 static AIHTMLDecoder *jabberHtmlEncoder = nil;
258 if (!jabberHtmlEncoder) {
259 jabberHtmlEncoder = [[AIHTMLDecoder alloc] init];
260 [jabberHtmlEncoder setIncludesHeaders:NO];
261 [jabberHtmlEncoder setIncludesFontTags:YES];
262 [jabberHtmlEncoder setClosesFontTags:YES];
263 [jabberHtmlEncoder setIncludesStyleTags:YES];
264 [jabberHtmlEncoder setIncludesColorTags:YES];
265 [jabberHtmlEncoder setEncodesNonASCII:NO];
266 [jabberHtmlEncoder setPreservesAllSpaces:NO];
267 [jabberHtmlEncoder setUsesAttachmentTextEquivalents:YES];
270 return [jabberHtmlEncoder encodeHTML:inAttributedString imagesPath:nil];
273 - (NSString *)_UIDForAddingObject:(AIListContact *)object
275 NSString *objectUID = [object UID];
278 if ([objectUID rangeOfString:@"@"].location != NSNotFound) {
279 properUID = objectUID;
281 properUID = [NSString stringWithFormat:@"%@@%@",objectUID,[self host]];
284 return [properUID lowercaseString];
287 - (NSString *)unknownGroupName {
288 return (AILocalizedString(@"Roster","Roster - the Jabber default group"));
291 - (NSString *)connectionStringForStep:(int)step
295 return AILocalizedString(@"Connecting",nil);
298 return AILocalizedString(@"Initializing Stream",nil);
301 return AILocalizedString(@"Reading data",nil);
304 return AILocalizedString(@"Authenticating",nil);
307 return AILocalizedString(@"Initializing Stream",nil);
310 return AILocalizedString(@"Authenticating",nil);
316 - (BOOL)shouldRequestRosterOnConnect
321 - (void)accountConnectionConnected
323 //HACK UNTIL LIBGAIM (broken as of [18051]) IS FIXED
324 if ([self shouldRequestRosterOnConnect]) {
325 JabberStream *js = account->gc->proto_data;
327 jabber_roster_request(js);
330 [super accountConnectionConnected];
333 - (BOOL)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
335 BOOL shouldReconnect = YES;
337 if (disconnectionError && *disconnectionError) {
338 if (([*disconnectionError rangeOfString:@"401"].location != NSNotFound) ||
339 ([*disconnectionError rangeOfString:@"Authentication Failure"].location != NSNotFound) ||
340 ([*disconnectionError rangeOfString:@"Not Authorized"].location != NSNotFound)) {
341 shouldReconnect = NO;
343 /* Automatic registration attempt */
344 //Display no error message
345 [*disconnectionError release];
346 *disconnectionError = nil;
348 [[adium interfaceController] displayQuestion:AILocalizedString(@"Would you like to register a new Jabber account?", nil)
349 withDescription:AILocalizedString(@"Jabber was unable to connect due to an invalid Jabber ID or password. This may be because you do not yet have an account on this Jabber server. Would you like to register now?",nil)
350 withWindowTitle:AILocalizedString(@"Invalid Jabber ID or Password",nil)
351 defaultButton:AILocalizedString(@"Register",nil)
352 alternateButton:AILocalizedString(@"Cancel",nil)
355 selector:@selector(answeredShouldReigsterNewJabberAccount:userInfo:)
358 } else if ([*disconnectionError rangeOfString:@"Stream Error"].location != NSNotFound) {
359 shouldReconnect = NO;
361 } else if ([*disconnectionError rangeOfString:@"requires plaintext authentication over an unencrypted stream"].location != NSNotFound) {
362 shouldReconnect = NO;
364 } else if ([*disconnectionError rangeOfString:@"Resource Conflict"].location != NSNotFound) {
365 shouldReconnect = NO;
369 return shouldReconnect;
372 - (BOOL)answeredShouldReigsterNewJabberAccount:(NSNumber *)returnCodeNumber userInfo:(id)userInfo
374 AITextAndButtonsReturnCode returnCode = [returnCodeNumber intValue];
376 switch (returnCode) {
377 case AITextAndButtonsDefaultReturn:
378 [self performSelector:@selector(performRegisterWithPassword:)
383 case AITextAndButtonsAlternateReturn:
384 case AITextAndButtonsOtherReturn:
385 case AITextAndButtonsClosedWithoutResponse:
386 [self serverReportedInvalidPassword];
393 - (void)disconnectFromDroppedNetworkConnection
395 /* Before we disconnect from a dropped network connection, set gc->disconnect_timeout to a non-0 value.
396 * This will let the prpl know that we are disconnecting with no backing ssl connection and that therefore
397 * the ssl connection is has should not be messaged in the process of disconnecting.
399 GaimConnection *gc = gaim_account_get_connection(account);
400 if (GAIM_CONNECTION_IS_VALID(gc) &&
401 !gc->disconnect_timeout) {
402 gc->disconnect_timeout = -1;
403 AILog(@"%@: Disconnecting from a dropped network connection", self);
406 [super disconnectFromDroppedNetworkConnection];
409 #pragma mark File transfer
410 - (BOOL)canSendFolders
415 - (void)beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
417 [super _beginSendOfFileTransfer:fileTransfer];
420 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
422 [super acceptFileTransferRequest:fileTransfer];
425 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
427 [super rejectFileReceiveRequest:fileTransfer];
430 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
432 [super cancelFileTransfer:fileTransfer];
435 #pragma mark Status Messages
436 - (NSAttributedString *)statusMessageForGaimBuddy:(GaimBuddy *)b
438 NSAttributedString *statusMessage = nil;
440 if (gaim_account_is_connected(account)) {
441 char *normalized = g_strdup(gaim_normalize(b->account, b->name));
444 if ((jb = jabber_buddy_find(account->gc->proto_data, normalized, FALSE))) {
445 NSString *statusMessageString = nil;
446 const char *msg = jabber_buddy_get_status_msg(jb);
449 //Get the custom jabber status message if one is set
450 statusMessageString = [NSString stringWithUTF8String:msg];
453 if (statusMessageString && [statusMessageString length]) {
454 statusMessage = [AIHTMLDecoder decodeHTML:statusMessageString];
461 return statusMessage;
464 - (NSString *)statusNameForGaimBuddy:(GaimBuddy *)buddy
466 NSString *statusName = nil;
467 GaimPresence *presence = gaim_buddy_get_presence(buddy);
468 GaimStatus *status = gaim_presence_get_active_status(presence);
469 const char *gaimStatusID = gaim_status_get_id(status);
471 if (!gaimStatusID) return nil;
473 if (!strcmp(gaimStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_CHAT))) {
474 statusName = STATUS_NAME_FREE_FOR_CHAT;
476 } else if (!strcmp(gaimStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_XA))) {
477 statusName = STATUS_NAME_EXTENDED_AWAY;
479 } else if (!strcmp(gaimStatusID, jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_DND))) {
480 statusName = STATUS_NAME_DND;
488 * @brief Jabber status messages are plaintext
490 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forStatusState:(AIStatus *)statusState
492 return [[inAttributedString attributedStringByConvertingLinksToStrings] string];
495 #pragma mark Menu items
496 - (NSString *)titleForContactMenuLabel:(const char *)label forContact:(AIListContact *)inContact
498 if (strcmp(label, "Un-hide From") == 0) {
499 return [NSString stringWithFormat:AILocalizedString(@"Un-hide From %@",nil),[inContact formattedUID]];
501 } else if (strcmp(label, "Temporarily Hide From") == 0) {
502 return [NSString stringWithFormat:AILocalizedString(@"Temporarily Hide From %@",nil),[inContact formattedUID]];
504 } else if (strcmp(label, "Unsubscribe") == 0) {
505 return [NSString stringWithFormat:AILocalizedString(@"Unsubscribe %@",nil),[inContact formattedUID]];
507 } else if (strcmp(label, "(Re-)Request authorization") == 0) {
508 return [NSString stringWithFormat:AILocalizedString(@"Re-request Authorization from %@",nil),[inContact formattedUID]];
510 } else if (strcmp(label, "Cancel Presence Notification") == 0) {
511 return [NSString stringWithFormat:AILocalizedString(@"Cancel Presence Notification to %@",nil),[inContact formattedUID]];
514 return [super titleForContactMenuLabel:label forContact:inContact];
517 #pragma mark Multiuser chat
519 //Multiuser chats come in with just the contact's name as contactName, but we want to actually do it right.
520 - (void)addUser:(NSString *)contactName toChat:(AIChat *)chat newArrival:(NSNumber *)newArrival
523 NSString *chatNameWithServer = [chat name];
524 NSString *chatParticipantName = [NSString stringWithFormat:@"%@/%@",chatNameWithServer,contactName];
525 AIListContact *listContact = [self contactWithUID:chatParticipantName];
527 [listContact setStatusObject:contactName forKey:@"FormattedUID" notify:YES];
529 [chat addParticipatingListObject:listContact notify:(newArrival && [newArrival boolValue])];
531 GaimDebug (@"Jabber: added user %@ to chat %@",chatParticipantName,chatNameWithServer);
535 - (oneway void)removeUser:(NSString *)contactName fromChat:(AIChat *)chat
538 NSString *chatNameWithServer = [chat name];
539 NSString *chatParticipantName = [NSString stringWithFormat:@"%@/%@",chatNameWithServer,contactName];
541 AIListContact *contact = [self contactWithUID:chatParticipantName];
543 [chat removeParticipatingListObject:contact];
545 GaimDebug (@"Jabber: removed user %@ to chat %@",chatParticipantName,chatNameWithServer);
551 * @brief Return the gaim status type to be used for a status
553 * Most subclasses should override this method; these generic values may be appropriate for others.
555 * Active services provided nonlocalized status names. An AIStatus is passed to this method along with a pointer
556 * to the status message. This method should handle any status whose statusNname this service set as well as any statusName
557 * defined in AIStatusController.h (which will correspond to the services handled by Adium by default).
558 * It should also handle a status name not specified in either of these places with a sane default, most likely by loooking at
559 * [statusState statusType] for a general idea of the status's type.
561 * @param statusState The status for which to find the gaim status ID
562 * @param arguments Prpl-specific arguments which will be passed with the state. Message is handled automatically.
564 * @result The gaim status ID
566 - (const char *)gaimStatusIDForStatus:(AIStatus *)statusState
567 arguments:(NSMutableDictionary *)arguments
569 const char *statusID = NULL;
570 NSString *statusName = [statusState statusName];
571 NSString *statusMessageString = [statusState statusMessageString];
572 NSNumber *priority = nil;
574 if (!statusMessageString) statusMessageString = @"";
576 switch ([statusState statusType]) {
577 case AIAvailableStatusType:
579 if (([statusName isEqualToString:STATUS_NAME_FREE_FOR_CHAT]) ||
580 ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_FREE_FOR_CHAT]] == NSOrderedSame))
581 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_CHAT);
582 priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AVAILABLE group:GROUP_ACCOUNT_STATUS];
586 case AIAwayStatusType:
588 if (([statusName isEqualToString:STATUS_NAME_DND]) ||
589 ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_DND]] == NSOrderedSame))
590 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_DND);
591 else if (([statusName isEqualToString:STATUS_NAME_EXTENDED_AWAY]) ||
592 ([statusMessageString caseInsensitiveCompare:[[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_EXTENDED_AWAY]] == NSOrderedSame))
593 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_XA);
594 priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AWAY group:GROUP_ACCOUNT_STATUS];
598 case AIInvisibleStatusType:
599 AILog(@"Warning: Invisibility is not yet supported in libgaim 2.0.0 jabber");
600 priority = [self preferenceForKey:KEY_JABBER_PRIORITY_AWAY group:GROUP_ACCOUNT_STATUS];
601 statusID = jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_AWAY);
602 // statusID = "Invisible";
605 case AIOfflineStatusType:
609 //Set our priority, which is actually set along with the status...Default is 0.
610 [arguments setObject:(priority ? priority : [NSNumber numberWithInt:0])
613 //If we didn't get a gaim status ID, request one from super
614 if (statusID == NULL) statusID = [super gaimStatusIDForStatus:statusState arguments:arguments];
619 #pragma mark Account Action Menu Items
620 - (NSString *)titleForAccountActionMenuLabel:(const char *)label
622 /* XXX All Jabber account actions depend upon adiumGaimRequestFields */